feat(gpui): gpui render backend and the macos-app target with native text layout - #293
Conversation
The portable backend family (wgpu, software raster, PPA, GLES2) keeps executing the DrawList against compile-time baked font atlases. This adds a second backend class on the same DrawList contract: engine/backends/gpui paints through Zed's gpui/Metal renderer, and text measurement + shaping move to the host text system (CoreText) when an app opts in. Core (backend-neutral, no behavior change unless a host installs the hook): - text::MeasureFn — a pluggable native text measurer on Fonts; Ui::set_text_measure installs it before mount. measure_run, the taffy MeasureCtx and the measureText op all route through the one provider. - DRAW_OP.TEXT_RUN (9): translation-only tracking-0 runs emit the run string + style through a DrawList side table; a styleHash word keeps identical word streams pixel-identical (demand-render hashes stay truthful). Tracked, scaled and rotated runs keep the baked GLYPH_RUN pair on BOTH the measure and paint sides. Partially clipped runs are scissor-bracketed. - raster/damage/wgpu interpreters learn to skip the op (it never reaches fixed-function backends); 5 new core tests pin the gates. Registry: text.layout.native capability + the macos-app target profile (hostAbi 3, window form, acceptsFixed — the slot platforms.ts reserved), so every fixed-viewport console demo admits unchanged, size-locked. engine/backends/gpui (standalone, like esp32p4-ppa): the DrawList -> gpui interpreter (quads, gradients, glyph-cell blitting for baked apps, content- mask scissors, TEXT_RUN shaping via shape_line with kern/liga off so prefix-sum caret math stays exact) plus a pixel-exact escape hatch: gouraud TRI / TEX_TRI batches raster through pocketjs_core::raster into cached local images at target density. hosts/macos (standalone lone-bin, like pocketbook): gpui window host of the macos-app target. Fixed 60 Hz guest ticks from a foreground timer governor (one guest.frame + surface.tick per tick, never from paint), demand renders off the DrawList hash, speaks note-widget's svc editor protocol (keyboard, pointer, scroll, clipboard, IME through EntityInputHandler), letterboxes fixed-viewport apps. bun run macos <app> resolves the manifest against macos-app and derives every host flag from the plan. Also: host string ops now decode JS strings lossily (LossyString) — an app measuring an emoji prefix sliced between surrogate halves is legal JS and must never abort the frame transaction. apps/note enhances text.layout.native: the same unmodified JSX markdown editor now runs with CoreText metrics, full CJK + color emoji, no runtime atlas baking. Proof: bun run macos note --proof (scripted click + typing, debounced autosave round-trips through the gpui host). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/bench-desktop.ts measures the pocket note on the gpui host against byte-identical web editors shelled by Tauri v2 and Electron: cold start to each app's own first-painted-frame READY report, hands-off idle and a 120 chars/s typing storm through each stack's real edit path, ps process- tree medians (WebKit XPC helpers attributed by spawn-delta — Tauri's WebContent/GPU processes are launchd children), footprint for physical memory. The gpui renderer gains shaped-line and measured-width caches so a keystroke repaint reshapes one line, not the document. docs/BACKENDS.md names the backend split; .github/workflows/macos.yml is the first macOS CI lane (core tests + clippy + host build). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nch fixtures Final measured run (M3 Max, methodology + fairness caveats in the report): the pocket note on the gpui host takes 1 process / 84 MB idle RSS / 145 ms cold start / 10 MB disk against Tauri v2 (4 procs, 192 MB, 391 ms) and Electron (5 procs, 382 MB, 328 ms, 242 MB). Storm completion is now verified (STORM-DONE), the Tauri window is explicitly focused (an unfocused WKWebView throttles timers and reads fiction), and the footprint column is dropped from the table — Electron's hardened helpers refuse task inspection, so RSS is the uniform metric. The benchmark fixtures' cargo target/ was packing into the npm tarball through the wholesale "tools" files entry (the v0.8.0 E415 failure mode — the tests/npm-package.test.ts tripwire caught it): tools/bench-desktop is now a governed negation in the files map. Tauri codegen (gen/) untracked; gpui backend passes clippy -D warnings for the macOS CI lane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The caret was animate-pulse — a continuous opacity sine that changes the DrawList every tick, so an idle editor repainted at 60 fps on every host (88% of ticks; ~3.6% CPU on both the gpui and wgpu hosts). Replace it with a browser-style square wave (app-local pocket.config.ts theme: 500 ms on / 500 ms off, hard edges via a same-frame twin keyframe) plus the browser input discipline: the caret is SOLID while typing or moving and resumes blinking from its ON phase after a 0.6 s rest (a <Show> swap remounts the animated node, restarting the baked timeline at frame 0). The square wave's constant segments keep the DrawList byte-stable between edges, so every demand-rendering host skips them: idle-in-edit repaints drop from 2115/2400 ticks to 83/2400 (~2 fps, exactly the edge count), CPU from 3.6% to 0.95% on the gpui host and 3.65% to 0.7% on the wgpu widget host. Same measurement protocol as docs/bench. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Double click selects the word under the pointer in BOTH modes, browser semantics throughout: whitespace and punctuation select as runs, a click just past a word's right half still selects the word (caretFromX rounds to the nearest boundary), CJK and surrogate pairs ride the non-ASCII word class, code blocks stay atomic in preview (rowSelSpan granularity), and a drag after the double click extends from the word start. Detection rides the virtual clock (0.4 s / 3 px on the svc press stream) so replays stay deterministic; wordRangeAt is regex-free QuickJS-portable math with unit coverage. The sample doc's 'same bytes as the PSP build' line predates the backend split — it now names the real contract (same core, same DrawList; wgpu paints baked atlases, gpui paints native text) and the two launch commands, and the charset-anchor comment drops the stale tofu caveat (runtime glyph baking and native shaping both exist now). Verified end-to-end on the gpui host: scripted double click on 'Pocket' + typing X autosaves '# X Note'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… svc-independent capabilities Three review blockers on #293, each fixed at the contract level: 1. TEXT_RUN carried a 32-bit content hash while the run string lived in a side table — colliding texts could leave identical word streams with different pixels, silently defeating demand-render hashes and damage word-diffs, and breaking the "DrawList is the complete Vec<u32>" contract. The op now packs the run string's UTF-8 bytes INTO the words (8 header words + payload; slot/align/lineHeight all in-stream) and the side table is gone: snapshots, hashes and diffs are exact by construction, not probabilistic. All interpreters skip the variable length; the gpui backend decodes from the stream. 2. Measurement and paint could disagree on rotated/scaled text (native- sized box, baked glyphs). The provider is now chosen ONCE at layout build (native iff a measurer is installed, tracking is 0 and the subtree declares no non-translation transform) and RECORDED on the node (Node::text_native); paint follows the record unconditionally, and the style-dirty restyle path can only flip it for a node-local tracking change. Rotated/transformed text takes the baked pair on BOTH sides — pinned by tests including a transformed-ancestor case and a paint-only-transform consistency case. 3. macos-app capabilities were conflated with the note's svc protocol. display.viewport.live is now host-generic: every dynamic app receives the framework's __pocketResizeViewport hook inside the tick transaction (the hero-resize regression); --editor derives from the app being the note companion, never from input.text; and the registry comment states the delivery paths plainly — buttons + live viewport host-generic, pointer/text/IME/clipboard via the companion adapter today (the macos-widget stock-host bar), with the host-generic pointer feed named as follow-up (the touch packing's 9-bit axes cannot carry a 4096-px window, so it needs new framework surface). Rot: hosts/macos passes clippy -D warnings and CI now runs it; the macOS workflow path filter covers contracts/spec, tools/macos.ts and apps/note; tools/macos.ts is negated out of the npm tarball (its build inputs are git-only — a published entry point that cannot run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Tauri/Electron comparison apps, the runner and the results move to a stacked PR so the backend/host/contract surface reviews on its own boundary. The npm files negation stays — it guards the re-add. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
三个 P1 均已在 17e1881 修复,rot 项一并处理,benchmark 已拆分为 stacked #294。逐条回应: 1. TEXT_RUN 哈希碰撞 → 词流现在直接携带字节。 你的判断成立:短哈希不能替代精确内容,且违反了 "DrawList 是完整 Vec" 的契约。修复取消了 side table 和 styleHash:TEXT_RUN 现在把 run 字符串的 UTF-8 字节打包进词流(8 个头部词 + payload,slot/align/lineHeight 全部随流),所有 hash/damage/快照消费者天然精确——是字节比较,不是摘要。各解释器按变长跳过; 2. 测量/绘制 provider 不一致 → provider 在布局构建期决策一次并记录在节点上。 原实现确实矛盾(native 高度布局 + baked 图集绘制,测试还钉错了语义)。现在 3. 能力与 Note 协议解耦。
Rot 项: hosts/macos 过 验证:core 119 测试、 |
… profile, windowed chrome, monospace code Review blockers: 1. Paint-only transforms (rotate/scale never relayout) could leave a text node's recorded provider permanently stale — native-measured box painted forever unrotated, and no way back. The draw walk now DETECTS the divergence (desired provider from the live world transform vs the record) and schedules the relayout that re-decides the pair: the stale frame lasts at most one tick, in both directions, with the 3D subtree path exempt (always the baked pair). Pinned by provider_self_heals_after_a_paint_only_transform. 2. macos-app declared input.text/pointer/ime/host.clipboard while only the note companion delivered them. The profile now registers exactly what the host implements for EVERY app — input.buttons, display.viewport.live, text.glyphs.baked, text.layout.native — and the registry comment names the companion delivery for the rest. The note's edit/pointer gates now track the COMPANION's runtime presence (connectSvc() !== null), not a capability id, so the flagship behaves identically while the contract stops over-promising. Flagship polish (user-reported): - Widget-era chrome is gated on platform.target === "macos-widget": a real window keeps OS corners (no rounded-xl card), resizes at its edges (no grip dots) and closes from its titlebar (no "Close widget" menu item). - Markdown code is monospace on every backend: MAX_FONT_SLOTS grows 16→24 with mono slots 16..18 (font-mono, 12/14/16 px), baked from a vendored JetBrains Mono (OFL) on the portable side and mapped through the host text system on gpui. The note's fenced blocks and inline code move to slot 17; note-widget's cjk mirror learns the mono rows. Rot: both new crates pass cargo fmt --check and CI enforces it; the macOS lane now actually exercises its TS path filter (contract drift, platform/ note/font-bake tests, and a real macos-app plan+bundle build of the note); docs/BACKENDS.md describes the bytes-in-words TEXT_RUN and the recorded provider instead of the deleted side table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
第二轮两个 P1 + 收尾项已在 0340e8d 处理: 1. 动态 transform 的 provider 失效 → 绘制期检测 + 自愈。 你的判断成立:"consistency beats fidelity" 把一次性权衡固化成了永久错误,且反方向(transform 移除后)永远回不到 native。现在 draw walk 在每个文本节点上比较"记录的 provider"与"当前 world 变换所要求的 provider"(3D 子树豁免),发现分歧即置脏——下一 tick 的 relayout 让测量与绘制一起重新决策。陈旧对最多存活一帧,双向收敛;穿越 identity 的循环动画每周期恰好重决策两次(注释里写明)。原测试改为钉住收敛语义: 2. macos-app 虚报能力 → 选了"从 profile 移除"分支。 现在 profile 只注册宿主对每个应用都实现的面: 次要项:
顺带的旗舰修缮(Evan 直接反馈): widget 时代的圆角卡片/右下 grip/"Close widget" 菜单项在 macos-app 上按 验证:core 119、 |
note.test.ts boots the sim host, which builds engine/wasm — the runner needs wasm32-unknown-unknown (and fmt --check needs rustfmt declared). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…adding The POCKET NOTE label is widget identity — a real window already titles itself in the OS titlebar, so the wordmark hides with the rest of the widget chrome (the eye/pencil toggle and menu stay). The content column's minimum side padding rises 22 -> 28 px, the floor narrow windows pin to, so text stops hugging the window edge; wide windows keep the centered 560 px column. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The POCKET NOTE wordmark hides off macos-widget now, and the sim boot is not a widget — assert the sample document instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… truthful svcOpen 1. The provider divergence check compared unlike predicates: layout gated on declared transforms down the path, the draw walk on the composed matrix being a translation — a parent scale canceled by a child's inverse satisfied one and not the other, oscillating layout.dirty into a permanent relayout loop, and every transform transition still painted one stale frame. Both sides now accumulate ONE shared predicate (Resolved::declares_transform) down identical recursions, so divergence can only mean a transform VALUE changed since the last relayout — and Ui::draw re-decides and REPAINTS before returning, so every frame that leaves draw() is provider-correct. Pinned by canceling_transforms_do_not_oscillate_the_provider (draws must not schedule spurious relayouts) and the zero-stale-frame rewrite of the transform enter/exit test. 2. svcOpen answered true for ANY service (allowlist default None), so the note believed its companion was live even when the host was launched without --editor — edit/pointer UI over a channel nobody feeds. The host now sets the allowlist before mount: exactly "note" with the adapter on, empty otherwise. Verified both ways: without --editor the scripted pencil-click + typing produces NO autosave (truthfully read-only); the --proof acceptance still passes with it. Minors: the macOS lane's path filter covers framework/compiler, the fonts, and the test files it executes; font-bake gains mono regression assertions (slot table stability + the monospace property itself: uniform baked advances where the proportional face differs); tools/macos.ts ships again with a git-checkout guard (the note.ts precedent — script and tool now agree) instead of the contradictory script-without-tool metadata; BACKENDS.md points at #294 for the benchmark; stale styleHash/bench comments in the host corrected; the vendored license loses its trailing whitespace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
第三轮两个 P1 已在 cca2d6e 修复: 1. provider 门谓词不等价 → 统一为单一共享谓词 + 同帧重试(零错误帧)。 你构造的抵消变换(父 scale-200 × 子 scale-50)确实会让两侧永不一致并形成持续 relayout——谓词分裂是我的设计错误。现在布局构建与绘制遍历沿完全相同的递归累积同一个谓词( 2. svcOpen 运行时虚报 → 宿主在 mount 前显式设置 allowlist。 次要项:
验证:core 120、 |
…ADY, honest comments - pocket-ui-surface: svcOpen now DENIES by default (the allowlist is a plain list, empty unless the host declares its companions) — the shared API is truthful-by-default instead of relying on every future host to remember the footgun. note-widget declares its "note" companion explicitly; a new surface test pins the default-deny. - The host's READY first-frame marker moves behind --announce-ready (the benchmark runner's flag, PR #294) — a production launch prints nothing. - The two core comments still describing a one-frame-stale provider now state the same-draw re-decide + repaint the code actually performs. - Companion-by-manifest-name is registered as architecture debt (issue #295) and referenced where the convention lives (tools/macos.ts). - The macOS lane watches package.json + tests/npm-package.test.ts and runs the npm tarball guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The benchmark from #293, split out for review-boundary hygiene: byte- identical web editors shelled by Tauri v2 and Electron against the pocket note on the gpui host, measured by tools/bench-desktop.ts (process-tree ps medians, WebKit XPC spawn-delta attribution, per-app first-frame READY, 120 chars/s typing storm through each stack's real edit path). Results + fairness caveats in docs/bench/gpui-vs-tauri-electron-2026-08-18.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
第四轮残留已全部处理(主分支尾部提交 + bench 分支 rebase):
验证:surface 6 测试(含 default-deny 新测试)、core 120、 |
…honest flag comments docs/BACKENDS.md states the actual invariant (same-draw relayout+repaint, zero stale frames — not next-tick healing); the macOS lane runs the pocket-ui-surface tests (svcOpen deny-by-default landed there); the tools/macos.ts flag comment names its two non-plan derivations and points both at #295, which also gains the plan-completeness (viewport policy) and Apple-sidecar-allowlist items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
第五轮两个必修项 + 三个债务标注已处理(尾部提交):
债务标注:
验证:tsc、--proof、CI 将随本推送重跑。 |
The benchmark from #293, split out for review-boundary hygiene: byte- identical web editors shelled by Tauri v2 and Electron against the pocket note on the gpui host, measured by tools/bench-desktop.ts (process-tree ps medians, WebKit XPC spawn-delta attribution, per-app first-frame READY, 120 chars/s typing storm through each stack's real edit path). Results + fairness caveats in docs/bench/gpui-vs-tauri-electron-2026-08-18.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…294) * feat(bench): desktop editor benchmark — gpui vs Tauri v2 vs Electron The benchmark from #293, split out for review-boundary hygiene: byte- identical web editors shelled by Tauri v2 and Electron against the pocket note on the gpui host, measured by tools/bench-desktop.ts (process-tree ps medians, WebKit XPC spawn-delta attribution, per-app first-frame READY, 120 chars/s typing storm through each stack's real edit path). Results + fairness caveats in docs/bench/gpui-vs-tauri-electron-2026-08-18.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(bench): re-measure on the review-fixed build, settle before idle sampling Numbers now come from the post-review TEXT_RUN code. The runner settles 20 s after READY before idle sampling (pcpu is a decaying average — early samples carried launch work into every app's idle median), and the report reads idle through the structural metric: the pocket governor receipt's repaint rate (84/2400 idle ticks, the caret square wave's edge count), since pcpu medians at low single digits drift ±1.5 points across runs for pocket and Tauri alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * bench: pass --announce-ready (the READY marker is opt-in on the host now) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…anifest and plan (#297) Closes #295. The companion adapter was selected by an app-name convention (tools/macos.ts matched manifest.name) and hosts derived size-locking by re-reading the raw manifest — the resolved plan was not the complete host boot truth. Now: - pocket.json gains app.companions: the exact svcOpen service names the app's adapters speak (schema-validated kebab names, unique). The note declares ["note"]. - The resolved plan carries `companions` and `viewport.policy` ("fixed" | "dynamic" — which manifest variant the target resolved), so every host flag derives from one artifact: tools/macos.ts drops both the manifest.name convention and the manifest re-read, passes --companions from the plan, and the host builds its svcOpen allowlist from exactly that list (deny-by-default underneath, unchanged). - The Apple sidecar gains its allowlist declaration surface: pocket_apple_set_svc_allowlist (before eval_bundle, like set_identity) — the deny-by-default gap flagged in #293 round 5. - Schema JSON regenerated from the TypeScript source; plan fixtures regenerated; the E7/device-profile plan expectations pin the new policy field (E7 is a window form — its plans are dynamic-policy). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The DrawList grows a second backend class: gpui (Zed's Metal renderer) on macOS, beside the portable baked-text family (wgpu, software raster, GE, PPA, GLES2). Same JSX + Tailwind guest, same deterministic frame transactions and animation engine — the one capability that legitimately differs per backend, text layout, becomes an explicit contract. Full design:
docs/BACKENDS.md.The text capability contract
text.glyphs.baked(portable): compile-time atlases, core-owned measurement,GLYPH_RUN, byte-deterministic pixels across hosts.text.layout.native(new): the host installs a core text measurer before the guest mounts (Ui::set_text_measure); taffy leaf sizes, themeasureTextop and painted glyphs all observe one provider — CoreText through gpui. Full OS font fallback (CJK, color emoji), no runtime atlas baking, no tofu. Pixels are deterministic per host, not across hosts — hence a different id, per the platforms.ts header rule.Core mechanics (backend-neutral; zero behavior change until a host installs the hook):
DRAW_OP.TEXT_RUN(9) packs the run string's UTF-8 bytes into the word stream (8 header words + payload) — the DrawList stays the completeVec<u32>pixel truth, so snapshots, demand-render hashes and damage word-diffs are exact by construction, never a digest.Resolved::declares_transform) down identical recursions and the decision is recorded on the node; when a paint-only transform changes the answer,Ui::drawre-decides and repaints before returning — zero stale frames, and canceling transforms (a parent scale inverted by a child) cannot oscillate the record. Tracked/scaled/rotated text keeps the baked pair on both sides; monospace is a real slot family (font-mono, slots 16..18, vendored JetBrains Mono) so code is monospace on every backend.The backend and host
engine/backends/gpui(standalone, likeesp32p4-ppa): the DrawList → gpui interpreter — vector quads/gradients, baked glyph-cell blitting for portable-text apps, scissors aswith_content_maskscopes, native shaping with kern/liga off (prefix-sum caret math stays exact), gouraudTRI/TEX_TRIbatches rastered throughpocketjs_core::rasteras a pixel-exact sub-backend, shaped-line + measured-width caches.hosts/macos(standalone lone-bin, likepocketbook): stock host of themacos-apptarget (hostAbi 3,form: "window",acceptsFixed). The profile registers exactly the host-generic surface —input.buttons,display.viewport.live(the__pocketResizeViewporthook fires for every dynamic app inside the tick transaction),text.glyphs.baked,text.layout.native. Fixed 60 Hz tick governor (never ticks from paint), demand-rendering off the DrawList hash, letterboxed fixed-viewport apps.--editor, andsvcOpenis deny-by-default in pocket-ui-surface — a host answers true only for companions it explicitly declares, so an absent adapter degrades apps truthfully to standalone. Explicit companion metadata in the manifest/plan is registered debt: contracts: model companion adapters explicitly in the manifest/build plan #295.bun run macos <app>(tools/macos.ts, ships with a git-checkout guard): capability-shaped flags (--fixed,--native-text) derive from the resolved plan;--editorselects the note companion (contracts: model companion adapters explicitly in the manifest/build plan #295).Flagship: the markdown editor
apps/noteruns from one source tree on every backend; on gpui it gets CoreText metrics, full CJK + color emoji input, monospace code blocks, browser-style editing polish (square-wave caret that demand rendering skips — idle repaints fell from 88% of ticks to ~2/s; double-click word selection on the virtual clock), and a real window's chrome (OS corners/resize/close — the widget-era card, grip and Close item gate off).bun run macos note --proof(scripted click + typing → autosave round-trips) and the negative proof — without--editor,svcOpenanswers false and the same script produces no autosave (truthfully read-only).LossyString).Benchmark
Lives in stacked #294 (harness, byte-identical Tauri/Electron comparison apps, results, fairness caveats). Headline: 1 process / 84 MB idle RSS / ~130-150 ms cold start / 10 MB disk against Electron's 5 / 382 MB / ~320 ms / 242 MB and Tauri's 4 / 193 MB / ~390 ms / 9 MB.
Review rounds — all resolved
__pocketResizeViewporthost-generic;--editordecoupled frominput.text.svcOpenallowlist set before mount; mono regression assertions; script/tool metadata agreement.svcOpenin the shared surface (+ note-widget declares its companion);READYbehind--announce-ready; stale core comments corrected; companion-by-name registered as contracts: model companion adapters explicitly in the manifest/build plan #295; npm tarball guards in the macOS lane.Known limitations / follow-ups
bun run gen).🤖 Generated with Claude Code